package com.nulldev.util.io.util;

import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;

public class CircularQueue<Element> {
	private final List<Element> queueArray;
	private final AtomicInteger front = new AtomicInteger();
	private final AtomicInteger rear = new AtomicInteger();
	private final AtomicInteger capacity = new AtomicInteger();
	public final AtomicInteger numberOfElements = new AtomicInteger();

	/**
	 * Constructor
	 * 
	 * @param size - the circular queue array size
	 */
	public CircularQueue(final int size) {
		this.capacity.set(size);
		this.front.set(-1);
		this.rear.set(-1);
		this.queueArray = new CopyOnWriteArrayList<Element>();
		this.numberOfElements.set(0);
	}

	public CircularQueue(final List<Element> list) {
		this.queueArray = new CopyOnWriteArrayList<Element>(list);
		this.capacity.set(list.size());
		this.front.set(0);
		this.rear.set(list.size() - 1);
		this.numberOfElements.set(list.size());
	}

	/**
	 * Get last element's index
	 * 
	 * @return first element's index in the circular queue's array
	 */
	public int getFront() {
		return this.front.get();
	}

	/**
	 * Get last element's index
	 * 
	 * @return last element's index in the circular queue's array
	 */
	public int getRear() {
		return this.rear.get();
	}

	/**
	 * Get number of elements in the circular queue
	 * 
	 * @return number of elements in the circular queue
	 */
	public int getNumberOfElements() {
		return this.numberOfElements.get();
	}

	public List<Element> getQueueArray() {
		return this.queueArray;
	}

	/**
	 * Check that queue is empty
	 * 
	 * @return true if there aren't elements in the circular queue
	 */
	public boolean isEmpty() {
		return this.numberOfElements.get() == 0;
	}

	/**
	 * Check that the circular queue is full
	 * 
	 * @return true if the circular queue is full
	 */
	public boolean isFull() {
		return this.numberOfElements.get() == this.capacity.get();
	}

	public void enqueueAndEnlarge(Element element) throws NoSuchElementException {
		this.capacity.incrementAndGet();
		this.enqueue(element);
	}

	/**
	 * Add an element to the circular queue
	 * 
	 * @param element - element to be added to the circular queue
	 * @throws NoSuchElementException
	 */
	public void enqueue(Element element) throws NoSuchElementException {
		if (isFull())
			throw new IllegalStateException("Queue is full");
		if (element == null)
			throw new NullPointerException("Element is null");
		else {
			if (isEmpty()) {
				rear.set(0);
				front.set(rear.get());
			}
			if (rear.get() > numberOfElements.get()) { // return to the beginning
				rear.set(0);
			}
			if (!queueArray.add(element))
				throw new UnsupportedOperationException("FAILED TO ADD: " + element);
			this.rear.incrementAndGet();
			this.numberOfElements.incrementAndGet();
		}
	}

	/**
	 * Get an element from the circular queue
	 * 
	 * @return element from the circular queue
	 */
	public Element dequeue() {
		if (isEmpty())
			throw new NoSuchElementException("Empty queue.");
		final Element result = queueArray.get(front.get());
		queueArray.remove(front.getAndIncrement());
		this.numberOfElements.decrementAndGet();
		return result;
	}

	/**
	 * Get an element from the circular queue
	 * 
	 * @return element from the circular queue
	 */
	public Element peek() {
		if (isEmpty())
			throw new NoSuchElementException("Empty queue.");
//		System.out.println("PEEK: front=" + front + ", ELEMENTS: " + this.numberOfElements);
		final Element result = queueArray.get(front.getAndIncrement());
		if (front.get() >= this.numberOfElements.get())
			front.set(0);
		return result;
	}

	public int size() {
		return this.numberOfElements.get();
	}

	public boolean remove(final Element obj) {
		final int idx = this.queueArray.indexOf(obj);
		if (this.queueArray.remove(obj)) {
			this.numberOfElements.decrementAndGet();
			if (idx == rear.get()) {
				rear.decrementAndGet();
			}
			return true;
		} else
			return false;
	}

	public boolean remove(final int idx) {
		final Element el = this.queueArray.remove(idx);
		if (el != null) {
			this.numberOfElements.decrementAndGet();
			if (idx == rear.get()) {
				rear.decrementAndGet();
			}
			return true;
		} else
			return false;
	}

	public Element at(final int i) {
		return this.queueArray.get(i);
	}

	public List<Element> list() {
		return this.queueArray;
	}
}
